#pragma once

#include <string>
#include <utility>
#include <vector>
#include <iostream>
#include <cassert>


namespace evm { namespace fs {

using namespace std;

class path {
public:
    typedef char value_type;
    typedef string string_type;

    /* Alternative directory separator which may be used in addition to the portable /.
     * On POSIX, this is the same forward slash / as the portable separator. */
    static constexpr char preferred_separator = '/';

    // constructors and destructor

    path() noexcept {}

    path(const path&) = default;

    path(path&& p) noexcept : pathname_(move(p.pathname_)), type_(p.type_) {
        split_cmpts();
        p.clear();
    }

    path(const string& p) : pathname_(p) { split_cmpts(); }

    path(const char* p) : pathname_(p) { split_cmpts(); }

    path(char p) : pathname_(1, p) { split_cmpts(); }

    ~path() = default;

    // assignments

    path& operator=(const path&) = default;

    path& operator=(path&& p) noexcept {
        pathname_ = move(p.pathname_);
        cmpts_ = move(p.cmpts_);
        type_ = p.type_;
        p.clear();
        return *this;
    }
    
    path& operator=(const string& p) { return *this = path(p); }

    path& operator=(const char* p) { return *this = path(p); }
    
    path& operator=(char p) { return *this = path(p); }


    template <typename Path>
    path& assign(Path const& p) { return *this = path(p); }

    // appends

    template <typename Path>
    path& operator/=(Path const& p) { return append(p); }

    path& append(const path& p) { return append_str(p.pathname_); }

    path& append(const string& p) { return append_str(p); }

    path& append(const char* p) { return append_str(string(p)); }

    path& append(char p) { return append_str(string(1, p)); }

    // concatenation

    path& operator+=(const path& p) {
        return operator+=(p.native());
    }

    path& operator+=(const string& p) {
        pathname_ += p;
        split_cmpts();
        return *this;
    }

    path& operator+=(const char* p) {
        pathname_ += p;
        split_cmpts();
        return *this;
    }

    path& operator+=(char p) {
        pathname_ += p;
        split_cmpts();
        return *this;
    }

    template <typename Path>
    path& concat(Path const& p) { return operator+=(p); }

    // modifiers

    void clear() noexcept {
        pathname_.clear();
        split_cmpts();
    }

    path& make_preferred() {
        return *this;
    }

    path& remove_filename();

    path& replace_filename(const path& __replacement);

    path& replace_extension(const path& __replacement = path());

    void swap(path& rhs) noexcept {
        pathname_.swap(rhs.pathname_);
        cmpts_.swap(rhs.cmpts_);
        std::swap(type_, rhs.type_);
    }

    // native format observers

    const string& native() const noexcept { return pathname_; }

    const char* c_str() const noexcept { return pathname_.c_str(); }

    operator string() const { return pathname_; }

    // compare

    int compare(const path& p) const noexcept;

    int compare(const string& p) const { return compare(path(p)); }

    int compare(const char* p) const { return compare(path(p)); }

    // decomposition

    path root_name() const;

    path root_directory() const;

    /* Returns the root path of the path. If the path does not include root path, returns path().
     * Effectively, returns the following: root_name() / root_directory() */
    path root_path() const;

    /* Returns path relative to root path. If *this is an empty path, returns an empty path. */
    path relative_path() const;

    /* Returns the path to the parent directory.
     * Returns empty path if empty() or there's only a single element in the path (begin() == --end().
     * The resulting path is constructed by appending all elements in a range [begin(), --end()) to an empty path. */
    path parent_path() const;

    path filename() const;

    path stem() const {
        auto ext = find_extension();
        if (ext.first && ext.second != 0)
            return path{ext.first->substr(0, ext.second)};
        return {};
    }

    path extension() const {
        auto ext = find_extension();
        if (ext.first && ext.second != string::npos)
            return path{ext.first->substr(ext.second)};
        return {};
    }

    // query

    bool empty() const noexcept { return pathname_.empty(); }

    bool has_root_name() const;

    bool has_root_directory() const;

    bool has_root_path() const;

    bool has_relative_path() const;

    bool has_parent_path() const;

    bool has_filename() const;

    bool has_stem() const {
        auto ext = find_extension();
        return ext.first && ext.second != 0;
    }

    bool has_extension() const {
        auto ext = find_extension();
        return ext.first && ext.second != string_type::npos;
    }

    bool is_absolute() const {
        return has_root_directory();
    }

    bool is_relative() const { return !is_absolute(); }

    // iterators
    class iterator;

    typedef iterator const_iterator;

    /* begin(): Returns an iterator to the first element of the path. If the path is empty, the returned iterator is equal to end().
     * end(): Returns an iterator one past the last element of the path. Dereferencing this iterator is undefined behavior.
     *
     * The sequence denoted by this pair of iterators consists of the following:
     * 1) root-name (if any)
     * 2) root-directory (if any)
     * 3) sequence of file-names, omitting any directory separators
     * 4) If there is a directory separator after the last file-name in the path,
     *    the last element before the end iterator is a fictitious dot file name. */

    iterator begin() const;

    iterator end() const;

private:
    enum class Type : unsigned char {
        MULTI, ROOT_NAME, ROOT_DIR, FILENAME
    };

    path(string str, Type type) : pathname_(str), type_(type) {
        assert(!empty());
        assert(type_ != Type::MULTI);
    }

    enum class Split {
        STEM, EXTENSION
    };

    path& append_str(const string& str) {
        if (!pathname_.empty() && !is_dir_sep(pathname_.back())
            && !str.empty() && !is_dir_sep(str.front()))
            pathname_ += preferred_separator;
        pathname_ += str;
        split_cmpts();
        return *this;
    }

    /* Return the pointer to the component, and the extension position in the component. */
    pair<const string*, size_t> find_extension() const;

    bool is_dir_sep(char ch) {
        return ch == '/';
    }

    void split_cmpts();

    void trim();

    void add_root_name(size_t n);

    void add_root_dir(size_t pos);

    void add_filename(size_t pos, size_t n);

    string pathname_;

    struct Cmpt;  // component type
    using List = vector<Cmpt>;  // compenents type

    List cmpts_;  // components, empty unless type_ == Type::MULTI
    Type type_ = Type::MULTI;
};

inline void swap(path& lhs, path& rhs) noexcept { lhs.swap(rhs); }

size_t hash_value(const path& p) noexcept;

// Compare paths

inline bool operator<(const path& lhs, const path& rhs) noexcept { return lhs.compare(rhs) < 0; }

inline bool operator<=(const path& lhs, const path& rhs) noexcept { return !(rhs < lhs); }

inline bool operator>(const path& lhs, const path& rhs) noexcept { return rhs < lhs; }

inline bool operator>=(const path& lhs, const path& rhs) noexcept { return !(lhs < rhs); }

inline bool operator==(const path& lhs, const path& rhs) noexcept { return lhs.compare(rhs) == 0; }

inline bool operator!=(const path& lhs, const path& rhs) noexcept { return !(lhs == rhs); }

/// Append one path to another
inline path operator/(const path& lhs, const path& rhs) { return path(lhs) /= rhs; }

/// Write a path to a stream
inline ostream& operator<<(ostream& os, const path& p) {
    os << string(p);
    return os;
}

/// Read a path from a stream
inline istream& operator>>(istream& is, path& p) {
    string tmp;
    if (is >> tmp)
        p = std::move(tmp);
    return is;
}

struct path::Cmpt : path {
    /* `s`, `t`: component path,
     * `pos`: position in the whole path. */
    Cmpt(string s, Type t, size_t pos)
    : path(move(s), t), pos_(pos) { }

    Cmpt() : pos_(-1) { }

    size_t pos_;
};

/// An iterator for the components of a path
class path::iterator {
public:
    using difference_type    = ptrdiff_t;
    using value_type        = path;
    using reference        = const path&;
    using pointer        = const path*;
    using iterator_category    = bidirectional_iterator_tag;

    iterator() : path_(nullptr), cur_(), at_end_() { }

    iterator(const iterator&) = default;

    iterator& operator=(const iterator&) = default;

    reference operator*() const;

    pointer operator->() const { return &**this; }

    iterator& operator++();

    iterator operator++(int) {
        auto tmp = *this;
        ++cur_;
        return tmp;
    }

    iterator& operator--();

    iterator operator--(int) {
        auto tmp = *this;
        --cur_;
        return tmp;
    }

    friend bool operator==(const iterator& lhs, const iterator& rhs) { return lhs.equals(rhs); }

    friend bool operator!=(const iterator& lhs, const iterator& rhs) { return !lhs.equals(rhs); }

private:
    friend class path;

    iterator(const path *path, path::List::const_iterator iter)
    : path_(path), cur_(iter), at_end_() { }

    iterator(const path *path, bool at_end)
    : path_(path), cur_(), at_end_(at_end) { }

    bool equals(iterator) const;

    const path* path_;
    path::List::const_iterator cur_;
    bool at_end_;  // only used when type != MULTI
};

inline path::iterator
path::begin() const {
    if (type_ == Type::MULTI)
        return iterator(this, cmpts_.begin());
    return iterator(this, false);
}

inline path::iterator
path::end() const {
    if (type_ == Type::MULTI)
        return iterator(this, cmpts_.end());
    return iterator(this, true);
}

inline path::iterator &
path::iterator::operator++() {
    assert(path_ != nullptr);
    if (path_->type_ == Type::MULTI) {
        assert(cur_ != path_->cmpts_.end());
        ++cur_;
    }
    else {
        assert(!at_end_);
        at_end_ = true;
    }
    return *this;
}

inline path::iterator &
path::iterator::operator--() {
    assert(path_ != nullptr);
    if (path_->type_ == Type::MULTI) {
        assert(cur_ != path_->cmpts_.begin());
        --cur_;
    }
    else {
        assert(at_end_);
        at_end_ = false;
    }
    return *this;
}

inline path::iterator::reference
path::iterator::operator*() const {
    assert(path_ != nullptr);
    if (path_->type_ == Type::MULTI) {
        assert(cur_ != path_->cmpts_.end());
        return *cur_;
    }
    return *path_;
}

inline bool
path::iterator::equals(iterator rhs) const {
    if (path_ != rhs.path_)
        return false;
    if (path_ == nullptr)
        return true;
    if (path_->type_ == path::Type::MULTI)
        return cur_ == rhs.cur_;
    return at_end_ == rhs.at_end_;
}

inline path path::filename() const {
    return empty() ? path() : *--end();
}

}}
